(function () {
    var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}

    // Used when there is no 'main' module.
    // The name is probably (hopefully) unique so minification removes for releases.
    var register_3795 = function (id) {
        var module = dem(id);
        var fragments = id.split('.');
        var target = Function('return this;')();
        for (var i = 0; i < fragments.length - 1; ++i) {
            if (target[fragments[i]] === undefined) { target[fragments[i]] = {}; }
            target = target[fragments[i]];
        }
        target[fragments[fragments.length - 1]] = module;
    };

    var instantiate = function (id) {
        var actual = defs[id];
        var dependencies = actual.deps;
        var definition = actual.defn;
        var len = dependencies.length;
        var instances = new Array(len);
        for (var i = 0; i < len; ++i) { instances[i] = dem(dependencies[i]); }
        var defResult = definition.apply(null, instances);
        if (defResult === undefined) { throw 'module [' + id + '] returned undefined'; }
        actual.instance = defResult;
    };

    var def = function (id, dependencies, definition) {
        if (typeof id !== 'string') { throw 'module id must be a string'; } else if (dependencies === undefined) { throw 'no dependencies for ' + id; } else if (definition === undefined) { throw 'no definition function for ' + id; }
        defs[id] = {
            deps: dependencies,
            defn: definition,
            instance: undefined
        };
    };

    var dem = function (id) {
        var actual = defs[id];
        if (actual === undefined) { throw 'module [' + id + '] was undefined'; } else if (actual.instance === undefined) { instantiate(id); }
        return actual.instance;
    };

    var req = function (ids, callback) {
        var len = ids.length;
        var instances = new Array(len);
        for (var i = 0; i < len; ++i) { instances[i] = dem(ids[i]); }
        callback.apply(null, instances);
    };

    var ephox = {};

    ephox.bolt = {
        module: {
            api: {
                define: def,
                require: req,
                demand: dem
            }
        }
    };

    var define = def;
    var require = req;
    var demand = dem;
    // this helps with minification when using a lot of global references
    var defineGlobal = function (id, ref) {
        define(id, [], function () { return ref; });
    };
    /* jsc
["tinymce.plugins.spellchecker.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.spellchecker.alien.DetectProPlugin","tinymce.plugins.spellchecker.api.Api","tinymce.plugins.spellchecker.api.Commands","tinymce.plugins.spellchecker.api.Settings","tinymce.plugins.spellchecker.ui.Buttons","tinymce.plugins.spellchecker.ui.SuggestionsMenu","global!tinymce.util.Tools.resolve","global!window","tinymce.plugins.spellchecker.core.Actions","tinymce.core.util.Tools","global!document","tinymce.core.dom.DOMUtils","tinymce.core.ui.Factory","tinymce.core.util.URI","tinymce.core.util.XHR","tinymce.plugins.spellchecker.api.Events","tinymce.plugins.spellchecker.core.DomTextMatcher"]
jsc */
    define(
        'ephox.katamari.api.Cell',

        [
        ],

        function () {
            var Cell = function (initial) {
                var value = initial;

                var get = function () {
                    return value;
                };

                var set = function (v) {
                    value = v;
                };

                var clone = function () {
                    return Cell(get());
                };

                return {
                    get: get,
                    set: set,
                    clone: clone
                };
            };

            return Cell;
        }
    );

    defineGlobal('global!tinymce.util.Tools.resolve', tinymce.util.Tools.resolve);
    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.PluginManager',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.PluginManager');
        }
    );

    defineGlobal('global!window', window);
    /**
 * DetectProPlugin.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.alien.DetectProPlugin',
        [
            'global!window',
            'tinymce.core.PluginManager'
        ],
        function (window, PluginManager) {
            var hasProPlugin = function (editor) {
                // draw back if power version is requested and registered
                if (/(^|[ ,])tinymcespellchecker([, ]|$)/.test(editor.settings.plugins) && PluginManager.get('tinymcespellchecker')) {
                    /* eslint no-console:0 */
                    if (typeof window.console !== 'undefined' && window.console.log) {
                        window.console.log(
                            'Spell Checker Pro is incompatible with Spell Checker plugin! ' +
            'Remove \'spellchecker\' from the \'plugins\' option.'
                        );
                    }
                    return true;
                } else {
                    return false;
                }
            };

            return {
                hasProPlugin: hasProPlugin
            };
        }
    );

    /**
 * Settings.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.api.Settings',
        [
        ],
        function () {
            var getLanguages = function (editor) {
                var defaultLanguages = 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,German=de,Italian=it,Polish=pl,Portuguese=pt_BR,Spanish=es,Swedish=sv';
                return editor.getParam('spellchecker_languages', defaultLanguages);
            };

            var getLanguage = function (editor) {
                var defaultLanguage = editor.getParam('language', 'en');
                return editor.getParam('spellchecker_language', defaultLanguage);
            };

            var getRpcUrl = function (editor) {
                return editor.getParam('spellchecker_rpc_url');
            };

            var getSpellcheckerCallback = function (editor) {
                return editor.getParam('spellchecker_callback');
            };

            var getSpellcheckerWordcharPattern = function (editor) {
                var defaultPattern = new RegExp('[^' +
      '\\s!"#$%&()*+,-./:;<=>?@[\\]^_{|}`' +
      '\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb' +
      '\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e\u00a0\u2002\u2003\u2009' +
      ']+', 'g');
                return editor.getParam('spellchecker_wordchar_pattern', defaultPattern);
            };

            return {
                getLanguages: getLanguages,
                getLanguage: getLanguage,
                getRpcUrl: getRpcUrl,
                getSpellcheckerCallback: getSpellcheckerCallback,
                getSpellcheckerWordcharPattern: getSpellcheckerWordcharPattern
            };
        }
    );
    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.util.Tools',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.util.Tools');
        }
    );

    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.util.URI',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.util.URI');
        }
    );

    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.util.XHR',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.util.XHR');
        }
    );

    /**
 * Events.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.api.Events',
        [
        ],
        function () {
            var fireSpellcheckStart = function (editor) {
                return editor.fire('SpellcheckStart');
            };

            var fireSpellcheckEnd = function (editor) {
                return editor.fire('SpellcheckEnd');
            };

            return {
                fireSpellcheckStart: fireSpellcheckStart,
                fireSpellcheckEnd: fireSpellcheckEnd
            };
        }
    );
    /**
 * DomTextMatcher.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.core.DomTextMatcher',
        [
        ],
        function () {
            function isContentEditableFalse (node) {
                return node && node.nodeType === 1 && node.contentEditable === 'false';
            }

            // Based on work developed by: James Padolsey http://james.padolsey.com
            // released under UNLICENSE that is compatible with LGPL
            // TODO: Handle contentEditable edgecase:
            // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
            return function (node, editor) {
                var m, matches = [], text, dom = editor.dom;
                var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;

                blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc
                hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
                shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT

                function createMatch (m, data) {
                    if (!m[0]) {
                        throw 'findAndReplaceDOMText cannot handle zero-length matches';
                    }

                    return {
                        start: m.index,
                        end: m.index + m[0].length,
                        text: m[0],
                        data: data
                    };
                }

                function getText (node) {
                    var txt;

                    if (node.nodeType === 3) {
                        return node.data;
                    }

                    if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
                        return '';
                    }

                    if (isContentEditableFalse(node)) {
                        return '\n';
                    }

                    txt = '';

                    if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
                        txt += '\n';
                    }

                    if ((node = node.firstChild)) {
                        do {
                            txt += getText(node);
                        } while ((node = node.nextSibling));
                    }

                    return txt;
                }

                function stepThroughMatches (node, matches, replaceFn) {
                    var startNode, endNode, startNodeIndex,
                        endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
                        matchLocation, matchIndex = 0;

                    matches = matches.slice(0);
                    matches.sort(function (a, b) {
                        return a.start - b.start;
                    });

                    matchLocation = matches.shift();

                    out: while (true) { // eslint-disable-line no-constant-condition
                        if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
                            atIndex++;
                        }

                        if (curNode.nodeType === 3) {
                            if (!endNode && curNode.length + atIndex >= matchLocation.end) {
                                // We've found the ending
                                endNode = curNode;
                                endNodeIndex = matchLocation.end - atIndex;
                            } else if (startNode) {
                                // Intersecting node
                                innerNodes.push(curNode);
                            }

                            if (!startNode && curNode.length + atIndex > matchLocation.start) {
                                // We've found the match start
                                startNode = curNode;
                                startNodeIndex = matchLocation.start - atIndex;
                            }

                            atIndex += curNode.length;
                        }

                        if (startNode && endNode) {
                            curNode = replaceFn({
                                startNode: startNode,
                                startNodeIndex: startNodeIndex,
                                endNode: endNode,
                                endNodeIndex: endNodeIndex,
                                innerNodes: innerNodes,
                                match: matchLocation.text,
                                matchIndex: matchIndex
                            });

                            // replaceFn has to return the node that replaced the endNode
                            // and then we step back so we can continue from the end of the
                            // match:
                            atIndex -= (endNode.length - endNodeIndex);
                            startNode = null;
                            endNode = null;
                            innerNodes = [];
                            matchLocation = matches.shift();
                            matchIndex++;

                            if (!matchLocation) {
                                break; // no more matches
                            }
                        } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
                            if (!isContentEditableFalse(curNode)) {
                                // Move down
                                curNode = curNode.firstChild;
                                continue;
                            }
                        } else if (curNode.nextSibling) {
                            // Move forward:
                            curNode = curNode.nextSibling;
                            continue;
                        }

                        // Move forward or up:
                        while (true) { // eslint-disable-line no-constant-condition
                            if (curNode.nextSibling) {
                                curNode = curNode.nextSibling;
                                break;
                            } else if (curNode.parentNode !== node) {
                                curNode = curNode.parentNode;
                            } else {
                                break out;
                            }
                        }
                    }
                }

                /**
      * Generates the actual replaceFn which splits up text nodes
      * and inserts the replacement element.
      */
                function genReplacer (callback) {
                    function makeReplacementNode (fill, matchIndex) {
                        var match = matches[matchIndex];

                        if (!match.stencil) {
                            match.stencil = callback(match);
                        }

                        var clone = match.stencil.cloneNode(false);
                        clone.setAttribute('data-mce-index', matchIndex);

                        if (fill) {
                            clone.appendChild(dom.doc.createTextNode(fill));
                        }

                        return clone;
                    }

                    return function (range) {
                        var before, after, parentNode, startNode = range.startNode,
                            endNode = range.endNode, matchIndex = range.matchIndex,
                            doc = dom.doc;

                        if (startNode === endNode) {
                            var node = startNode;

                            parentNode = node.parentNode;
                            if (range.startNodeIndex > 0) {
                                // Add "before" text node (before the match)
                                before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
                                parentNode.insertBefore(before, node);
                            }

                            // Create the replacement node:
                            var el = makeReplacementNode(range.match, matchIndex);
                            parentNode.insertBefore(el, node);
                            if (range.endNodeIndex < node.length) {
                                // Add "after" text node (after the match)
                                after = doc.createTextNode(node.data.substring(range.endNodeIndex));
                                parentNode.insertBefore(after, node);
                            }

                            node.parentNode.removeChild(node);

                            return el;
                        }

                        // Replace startNode -> [innerNodes...] -> endNode (in that order)
                        before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
                        after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
                        var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
                        var innerEls = [];

                        for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
                            var innerNode = range.innerNodes[i];
                            var innerEl = makeReplacementNode(innerNode.data, matchIndex);
                            innerNode.parentNode.replaceChild(innerEl, innerNode);
                            innerEls.push(innerEl);
                        }

                        var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);

                        parentNode = startNode.parentNode;
                        parentNode.insertBefore(before, startNode);
                        parentNode.insertBefore(elA, startNode);
                        parentNode.removeChild(startNode);

                        parentNode = endNode.parentNode;
                        parentNode.insertBefore(elB, endNode);
                        parentNode.insertBefore(after, endNode);
                        parentNode.removeChild(endNode);

                        return elB;
                    };
                }

                function unwrapElement (element) {
                    var parentNode = element.parentNode;
                    parentNode.insertBefore(element.firstChild, element);
                    element.parentNode.removeChild(element);
                }

                function hasClass (elm) {
                    return elm.className.indexOf('mce-spellchecker-word') !== -1;
                }

                function getWrappersByIndex (index) {
                    var elements = node.getElementsByTagName('*'), wrappers = [];

                    index = typeof index === 'number' ? '' + index : null;

                    for (var i = 0; i < elements.length; i++) {
                        var element = elements[i], dataIndex = element.getAttribute('data-mce-index');

                        if (dataIndex !== null && dataIndex.length && hasClass(element)) {
                            if (dataIndex === index || index === null) {
                                wrappers.push(element);
                            }
                        }
                    }

                    return wrappers;
                }

                /**
      * Returns the index of a specific match object or -1 if it isn't found.
      *
      * @param  {Match} match Text match object.
      * @return {Number} Index of match or -1 if it isn't found.
      */
                function indexOf (match) {
                    var i = matches.length;
                    while (i--) {
                        if (matches[i] === match) {
                            return i;
                        }
                    }

                    return -1;
                }

                /**
      * Filters the matches. If the callback returns true it stays if not it gets removed.
      *
      * @param {Function} callback Callback to execute for each match.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function filter (callback) {
                    var filteredMatches = [];

                    each(function (match, i) {
                        if (callback(match, i)) {
                            filteredMatches.push(match);
                        }
                    });

                    matches = filteredMatches;

                    /* jshint validthis:true */
                    return this;
                }

                /**
      * Executes the specified callback for each match.
      *
      * @param {Function} callback  Callback to execute for each match.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function each (callback) {
                    for (var i = 0, l = matches.length; i < l; i++) {
                        if (callback(matches[i], i) === false) {
                            break;
                        }
                    }

                    /* jshint validthis:true */
                    return this;
                }

                /**
      * Wraps the current matches with nodes created by the specified callback.
      * Multiple clones of these matches might occur on matches that are on multiple nodex.
      *
      * @param {Function} callback Callback to execute in order to create elements for matches.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function wrap (callback) {
                    if (matches.length) {
                        stepThroughMatches(node, matches, genReplacer(callback));
                    }

                    /* jshint validthis:true */
                    return this;
                }

                /**
      * Finds the specified regexp and adds them to the matches collection.
      *
      * @param {RegExp} regex Global regexp to search the current node by.
      * @param {Object} [data] Optional custom data element for the match.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function find (regex, data) {
                    if (text && regex.global) {
                        while ((m = regex.exec(text))) {
                            matches.push(createMatch(m, data));
                        }
                    }

                    return this;
                }

                /**
      * Unwraps the specified match object or all matches if unspecified.
      *
      * @param {Object} [match] Optional match object.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function unwrap (match) {
                    var i, elements = getWrappersByIndex(match ? indexOf(match) : null);

                    i = elements.length;
                    while (i--) {
                        unwrapElement(elements[i]);
                    }

                    return this;
                }

                /**
      * Returns a match object by the specified DOM element.
      *
      * @param {DOMElement} element Element to return match object for.
      * @return {Object} Match object for the specified element.
      */
                function matchFromElement (element) {
                    return matches[element.getAttribute('data-mce-index')];
                }

                /**
      * Returns a DOM element from the specified match element. This will be the first element if it's split
      * on multiple nodes.
      *
      * @param {Object} match Match element to get first element of.
      * @return {DOMElement} DOM element for the specified match object.
      */
                function elementFromMatch (match) {
                    return getWrappersByIndex(indexOf(match))[0];
                }

                /**
      * Adds match the specified range for example a grammar line.
      *
      * @param {Number} start Start offset.
      * @param {Number} length Length of the text.
      * @param {Object} data Custom data object for match.
      * @return {DomTextMatcher} Current DomTextMatcher instance.
      */
                function add (start, length, data) {
                    matches.push({
                        start: start,
                        end: start + length,
                        text: text.substr(start, length),
                        data: data
                    });

                    return this;
                }

                /**
      * Returns a DOM range for the specified match.
      *
      * @param  {Object} match Match object to get range for.
      * @return {DOMRange} DOM Range for the specified match.
      */
                function rangeFromMatch (match) {
                    var wrappers = getWrappersByIndex(indexOf(match));

                    var rng = editor.dom.createRng();
                    rng.setStartBefore(wrappers[0]);
                    rng.setEndAfter(wrappers[wrappers.length - 1]);

                    return rng;
                }

                /**
      * Replaces the specified match with the specified text.
      *
      * @param {Object} match Match object to replace.
      * @param {String} text Text to replace the match with.
      * @return {DOMRange} DOM range produced after the replace.
      */
                function replace (match, text) {
                    var rng = rangeFromMatch(match);

                    rng.deleteContents();

                    if (text.length > 0) {
                        rng.insertNode(editor.dom.doc.createTextNode(text));
                    }

                    return rng;
                }

                /**
      * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches.
      *
      * @return {[type]} [description]
      */
                function reset () {
                    matches.splice(0, matches.length);
                    unwrap();

                    return this;
                }

                text = getText(node);

                return {
                    text: text,
                    matches: matches,
                    each: each,
                    filter: filter,
                    reset: reset,
                    matchFromElement: matchFromElement,
                    elementFromMatch: elementFromMatch,
                    find: find,
                    add: add,
                    wrap: wrap,
                    unwrap: unwrap,
                    replace: replace,
                    rangeFromMatch: rangeFromMatch,
                    indexOf: indexOf
                };
            };
        }
    );
    /**
 * Actions.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.core.Actions',
        [
            'tinymce.core.util.Tools',
            'tinymce.core.util.URI',
            'tinymce.core.util.XHR',
            'tinymce.plugins.spellchecker.api.Events',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.DomTextMatcher'
        ],
        function (Tools, URI, XHR, Events, Settings, DomTextMatcher) {
            var getTextMatcher = function (editor, textMatcherState) {
                if (!textMatcherState.get()) {
                    var textMatcher = new DomTextMatcher(editor.getBody(), editor);
                    textMatcherState.set(textMatcher);
                }

                return textMatcherState.get();
            };

            var isEmpty = function (obj) {
                for (var name in obj) {
                    return false;
                }

                return true;
            };

            var defaultSpellcheckCallback = function (editor, pluginUrl, currentLanguageState) {
                return function (method, text, doneCallback, errorCallback) {
                    var data = { method: method, lang: currentLanguageState.get() }, postData = '';

                    data[method === 'addToDictionary' ? 'word' : 'text'] = text;

                    Tools.each(data, function (value, key) {
                        if (postData) {
                            postData += '&';
                        }

                        postData += key + '=' + encodeURIComponent(value);
                    });

                    XHR.send({
                        url: new URI(pluginUrl).toAbsolute(Settings.getRpcUrl(editor)),
                        type: 'post',
                        content_type: 'application/x-www-form-urlencoded',
                        data: postData,
                        success: function (result) {
                            result = JSON.parse(result);

                            if (!result) {
                                var message = editor.translate('Server response wasn\'t proper JSON.');
                                errorCallback(message);
                            } else if (result.error) {
                                errorCallback(result.error);
                            } else {
                                doneCallback(result);
                            }
                        },
                        error: function () {
                            var message = editor.translate('The spelling service was not found: (') +
              Settings.getRpcUrl(editor) +
              editor.translate(')');
                            errorCallback(message);
                        }
                    });
                };
            };

            var sendRpcCall = function (editor, pluginUrl, currentLanguageState, name, data, successCallback, errorCallback) {
                var userSpellcheckCallback = Settings.getSpellcheckerCallback(editor);
                var spellCheckCallback = userSpellcheckCallback || defaultSpellcheckCallback(editor, pluginUrl, currentLanguageState);
                spellCheckCallback.call(editor.plugins.spellchecker, name, data, successCallback, errorCallback);
            };

            var spellcheck = function (editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
                if (finish(editor, startedState, textMatcherState)) {
                    return;
                }

                var errorCallback = function (message) {
                    editor.notificationManager.open({ text: message, type: 'error' });
                    editor.setProgressState(false);
                    finish(editor, startedState, textMatcherState);
                };

                var successCallback = function (data) {
                    markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data);
                };

                editor.setProgressState(true);
                sendRpcCall(editor, pluginUrl, currentLanguageState, 'spellcheck', getTextMatcher(editor, textMatcherState).text, successCallback, errorCallback);
                editor.focus();
            };

            var checkIfFinished = function (editor, startedState, textMatcherState) {
                if (!editor.dom.select('span.mce-spellchecker-word').length) {
                    finish(editor, startedState, textMatcherState);
                }
            };

            var addToDictionary = function (editor, pluginUrl, startedState, textMatcherState, word, spans) {
                editor.setProgressState(true);

                sendRpcCall(editor, pluginUrl, 'addToDictionary', word, function () {
                    editor.setProgressState(false);
                    editor.dom.remove(spans, true);
                    checkIfFinished(editor, startedState, textMatcherState);
                }, function (message) {
                    editor.notificationManager.open({ text: message, type: 'error' });
                    editor.setProgressState(false);
                });
            };

            var ignoreWord = function (editor, startedState, textMatcherState, word, spans, all) {
                editor.selection.collapse();

                if (all) {
                    Tools.each(editor.dom.select('span.mce-spellchecker-word'), function (span) {
                        if (span.getAttribute('data-mce-word') === word) {
                            editor.dom.remove(span, true);
                        }
                    });
                } else {
                    editor.dom.remove(spans, true);
                }

                checkIfFinished(editor, startedState, textMatcherState);
            };

            var finish = function (editor, startedState, textMatcherState) {
                getTextMatcher(editor, textMatcherState).reset();
                textMatcherState.set(null);

                if (startedState.get()) {
                    startedState.set(false);
                    Events.fireSpellcheckEnd(editor);
                    return true;
                }
            };

            var getElmIndex = function (elm) {
                var value = elm.getAttribute('data-mce-index');

                if (typeof value === 'number') {
                    return '' + value;
                }

                return value;
            };

            var findSpansByIndex = function (editor, index) {
                var nodes, spans = [];

                nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
                if (nodes.length) {
                    for (var i = 0; i < nodes.length; i++) {
                        var nodeIndex = getElmIndex(nodes[i]);

                        if (nodeIndex === null || !nodeIndex.length) {
                            continue;
                        }

                        if (nodeIndex === index.toString()) {
                            spans.push(nodes[i]);
                        }
                    }
                }

                return spans;
            };

            var markErrors = function (editor, startedState, textMatcherState, lastSuggestionsState, data) {
                var suggestions, hasDictionarySupport;

                if (data.words) {
                    hasDictionarySupport = !!data.dictionary;
                    suggestions = data.words;
                } else {
                    // Fallback to old format
                    suggestions = data;
                }

                editor.setProgressState(false);

                if (isEmpty(suggestions)) {
                    var message = editor.translate('No misspellings found.');
                    editor.notificationManager.open({ text: message, type: 'info' });
                    startedState.set(false);
                    return;
                }

                lastSuggestionsState.set({
                    suggestions: suggestions,
                    hasDictionarySupport: hasDictionarySupport
                });

                getTextMatcher(editor, textMatcherState).find(Settings.getSpellcheckerWordcharPattern(editor)).filter(function (match) {
                    return !!suggestions[match.text];
                }).wrap(function (match) {
                    return editor.dom.create('span', {
                        'class': 'mce-spellchecker-word',
                        'data-mce-bogus': 1,
                        'data-mce-word': match.text
                    });
                });

                startedState.set(true);
                Events.fireSpellcheckStart(editor);
            };

            return {
                spellcheck: spellcheck,
                checkIfFinished: checkIfFinished,
                addToDictionary: addToDictionary,
                ignoreWord: ignoreWord,
                findSpansByIndex: findSpansByIndex,
                getElmIndex: getElmIndex,
                markErrors: markErrors
            };
        }
    );
    /**
 * Api.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.api.Api',
        [
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
        function (Settings, Actions) {
            var get = function (editor, startedState, lastSuggestionsState, textMatcherState, url) {
                var getLanguage = function () {
                    return Settings.getLanguage(editor);
                };

                var getWordCharPattern = function () {
                    return Settings.getSpellcheckerWordcharPattern(editor);
                };

                var markErrors = function (data) {
                    Actions.markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data);
                };

                var getTextMatcher = function () {
                    return textMatcherState.get();
                };

                return {
                    getTextMatcher: getTextMatcher,
                    getWordCharPattern: getWordCharPattern,
                    markErrors: markErrors,
                    getLanguage: getLanguage
                };
            };

            return {
                get: get
            };
        }
    );
    /**
 * Commands.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.api.Commands',
        [
            'tinymce.plugins.spellchecker.core.Actions'
        ],
        function (Actions) {
            var register = function (editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
                editor.addCommand('mceSpellCheck', function () {
                    Actions.spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
                });
            };

            return {
                register: register
            };
        }
    );
    /**
 * Buttons.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.ui.Buttons',
        [
            'tinymce.core.util.Tools',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
        function (Tools, Settings, Actions) {
            var buildMenuItems = function (listName, languageValues) {
                var items = [];

                Tools.each(languageValues, function (languageValue) {
                    items.push({
                        selectable: true,
                        text: languageValue.name,
                        data: languageValue.value
                    });
                });

                return items;
            };

            var updateSelection = function (editor) {
                return function (e) {
                    var selectedLanguage = Settings.getLanguage(editor);

                    e.control.items().each(function (ctrl) {
                        ctrl.active(ctrl.settings.data === selectedLanguage);
                    });
                };
            };

            var getItems = function (editor) {
                return Tools.map(Settings.getLanguages(editor).split(','), function (langPair) {
                    langPair = langPair.split('=');

                    return {
                        name: langPair[0],
                        value: langPair[1]
                    };
                });
            };

            var register = function (editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState) {
                var languageMenuItems = buildMenuItems('Language', getItems(editor));
                var startSpellchecking = function () {
                    Actions.spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
                };

                var buttonArgs = {
                    tooltip: 'Spellcheck',
                    onclick: startSpellchecking,
                    onPostRender: function (e) {
                        var ctrl = e.control;

                        editor.on('SpellcheckStart SpellcheckEnd', function () {
                            ctrl.active(startedState.get());
                        });
                    }
                };

                if (languageMenuItems.length > 1) {
                    buttonArgs.type = 'splitbutton';
                    buttonArgs.menu = languageMenuItems;
                    buttonArgs.onshow = updateSelection(editor);
                    buttonArgs.onselect = function (e) {
                        currentLanguageState.set(e.control.settings.data);
                    };
                }

                editor.addButton('spellchecker', buttonArgs);

                editor.addMenuItem('spellchecker', {
                    text: 'Spellcheck',
                    context: 'tools',
                    onclick: startSpellchecking,
                    selectable: true,
                    onPostRender: function () {
                        var self = this;

                        self.active(startedState.get());

                        editor.on('SpellcheckStart SpellcheckEnd', function () {
                            self.active(startedState.get());
                        });
                    }
                });
            };

            return {
                register: register
            };
        }
    );
    defineGlobal('global!document', document);
    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.dom.DOMUtils',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.dom.DOMUtils');
        }
    );

    /**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.core.ui.Factory',
        [
            'global!tinymce.util.Tools.resolve'
        ],
        function (resolve) {
            return resolve('tinymce.ui.Factory');
        }
    );

    /**
 * SuggestionsMenu.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.ui.SuggestionsMenu',
        [
            'global!document',
            'tinymce.core.dom.DOMUtils',
            'tinymce.core.ui.Factory',
            'tinymce.core.util.Tools',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
        function (document, DOMUtils, Factory, Tools, Settings, Actions) {
            var suggestionsMenu;

            var showSuggestions = function (editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, word, spans) {
                var items = [], suggestions = lastSuggestionsState.get().suggestions[word];

                Tools.each(suggestions, function (suggestion) {
                    items.push({
                        text: suggestion,
                        onclick: function () {
                            editor.insertContent(editor.dom.encode(suggestion));
                            editor.dom.remove(spans);
                            Actions.checkIfFinished(editor, startedState, textMatcherState);
                        }
                    });
                });

                items.push({ text: '-' });

                var hasDictionarySupport = lastSuggestionsState.get().hasDictionarySupport;
                if (hasDictionarySupport) {
                    items.push({
                        text: 'Add to Dictionary',
                        onclick: function () {
                            Actions.addToDictionary(editor, pluginUrl, startedState, textMatcherState, word, spans);
                        }
                    });
                }

                items.push.apply(items, [
                    {
                        text: 'Ignore',
                        onclick: function () {
                            Actions.ignoreWord(editor, startedState, textMatcherState, word, spans);
                        }
                    },

                    {
                        text: 'Ignore all',
                        onclick: function () {
                            Actions.ignoreWord(editor, startedState, textMatcherState, word, spans, true);
                        }
                    }
                ]);

                // Render menu
                suggestionsMenu = Factory.create('menu', {
                    items: items,
                    context: 'contextmenu',
                    onautohide: function (e) {
                        if (e.target.className.indexOf('spellchecker') !== -1) {
                            e.preventDefault();
                        }
                    },
                    onhide: function () {
                        suggestionsMenu.remove();
                        suggestionsMenu = null;
                    }
                });

                suggestionsMenu.renderTo(document.body);

                // Position menu
                var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer());
                var targetPos = editor.dom.getPos(spans[0]);
                var root = editor.dom.getRoot();

                // Adjust targetPos for scrolling in the editor
                if (root.nodeName === 'BODY') {
                    targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
                    targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
                } else {
                    targetPos.x -= root.scrollLeft;
                    targetPos.y -= root.scrollTop;
                }

                pos.x += targetPos.x;
                pos.y += targetPos.y;

                suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight);
            };

            var setup = function (editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState) {
                editor.on('click', function (e) {
                    var target = e.target;

                    if (target.className === 'mce-spellchecker-word') {
                        e.preventDefault();

                        var spans = Actions.findSpansByIndex(editor, Actions.getElmIndex(target));

                        if (spans.length > 0) {
                            var rng = editor.dom.createRng();
                            rng.setStartBefore(spans[0]);
                            rng.setEndAfter(spans[spans.length - 1]);
                            editor.selection.setRng(rng);
                            showSuggestions(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, target.getAttribute('data-mce-word'), spans);
                        }
                    }
                });
            };

            return {
                setup: setup
            };
        }
    );

    /**
 * Plugin.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
        'tinymce.plugins.spellchecker.Plugin',
        [
            'ephox.katamari.api.Cell',
            'tinymce.core.PluginManager',
            'tinymce.plugins.spellchecker.alien.DetectProPlugin',
            'tinymce.plugins.spellchecker.api.Api',
            'tinymce.plugins.spellchecker.api.Commands',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.ui.Buttons',
            'tinymce.plugins.spellchecker.ui.SuggestionsMenu'
        ],
        function (Cell, PluginManager, DetectProPlugin, Api, Commands, Settings, Buttons, SuggestionsMenu) {
            PluginManager.add('spellchecker', function (editor, pluginUrl) {
                var startedState = Cell(false);
                var currentLanguageState = Cell(Settings.getLanguage(editor));
                var textMatcherState = Cell(null);
                var lastSuggestionsState = Cell({});

                if (DetectProPlugin.hasProPlugin(editor) === false) {
                    Buttons.register(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState);
                    SuggestionsMenu.setup(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState);
                    Commands.register(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
                }

                return Api.get(editor, startedState, lastSuggestionsState, textMatcherState, pluginUrl);
            });

            return function () { };
        }
    );
    dem('tinymce.plugins.spellchecker.Plugin')();
})();
